﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Web;
using System.Web.Mvc;

namespace TotalFreedomKO
{
    public enum KoBindingOptions
    {
        None = 0,
        IncludeData = 1,
        LoadDataOnStart = 2,
        OnlyKoProperties = 4,
    }


    public interface IKoHelper<TModel>
    {
        HtmlHelper HtmlHelper { get; }
    }

    public class KoHelper<TModel> : IKoHelper<TModel>
    {
        public HtmlHelper HtmlHelper
        {
            get;
            private set;
        }

        public KoHelper(HtmlHelper htmlHelper)
        {
            HtmlHelper = htmlHelper;
        }
    }

    public interface IKoViewModel
    {
    }

    public class BaseKoViewModel : IKoViewModel
    {
        private static bool IsComputed(PropertyInfo propertyInfo)
        {
            if (propertyInfo != null)
            {
                object[] attrs = propertyInfo.GetCustomAttributes(typeof(KoComputed), true);
                if (attrs != null && attrs.Length > 0)
                    return true;
            }

            return false;
        }

        private static string GetTypesDefinitions(Type currentType, List<string> includedTypes, object obj)
        {
            // if already included it once, no need to do it again
            if (includedTypes.IndexOf(currentType.Name) >= 0)
                return "";

            if (currentType.IsPrimitive || currentType == typeof(String) || currentType == typeof(Decimal))
                return "";

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("\tfunction " + currentType.Name + "() {");
            sb.AppendLine("\t\tself = this;");
            includedTypes.Add(currentType.Name);

            foreach (MemberInfo mi in currentType.GetMembers())
            {
                if (mi.MemberType == MemberTypes.Property)
                {
                    PropertyInfo propertyInfo = (PropertyInfo)mi;
                    Type type = propertyInfo.PropertyType;

                    if (type.IsPrimitive || type == typeof(String) || type == typeof(Decimal))
                    {
                        if (IsComputed(propertyInfo))
                        {
                            if (obj != null)
                            {
                                object v = propertyInfo.GetValue(obj, null);
                                if (v != null)
                                    sb.AppendLine("\t\tthis." + propertyInfo.Name + " = ko.computed( function(){" + v.ToString() + " });");
                            }
                        }
                        else
                            sb.AppendLine("\t\tthis." + propertyInfo.Name + " = ko.observable();");
                    }
                    else if (typeof(IEnumerable).IsAssignableFrom(type))
                    {
                        if (type.IsGenericType)
                        {
                            Type genericType = type.GetGenericTypeDefinition();
                            if (genericType == typeof(IList<>) || genericType == typeof(List<>))
                            {
                                Type itemType = type.GetGenericArguments()[0];
                                string s = GetTypesDefinitions(itemType, includedTypes, obj);
                                sb.Insert(0, s);
                            }
                        }
                        else if (type.IsArray)
                        {
                            string s = GetTypesDefinitions(type.GetElementType(), includedTypes, obj);
                            sb.Insert(0, s);
                        }

                        sb.AppendLine("\t\tthis." + propertyInfo.Name + " = ko.observableArray([]);");
                    }
                    else
                    {
                        string s = GetTypesDefinitions(type, includedTypes, obj);
                        sb.Insert(0, s);

                        sb.AppendLine("\t\tthis." + propertyInfo.Name + " = ko.observable(); // object of " + type.Name);
                    }
                }
            }

            sb.AppendLine("\t}");
            sb.AppendLine("");

            return sb.ToString();
        }

        public static string GetJSON(object obj)
        {
            return new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(obj);
        }

        private static string GetData(object obj, int level = 0)
        {
            if (obj == null)
                return "null";

            Type currentType = obj.GetType();

            if (currentType.IsPrimitive || currentType == typeof(String) || currentType == typeof(Decimal))
                return GetJSON(obj);            

            int level1 = level + 1;

            StringBuilder sb = new StringBuilder();
            string parentName;
            if (level > 0)
            {
                parentName = "data" + level1;
                sb.AppendLine("\t(function(){ var data" + level1 + " = new " + currentType.Name + "();");
            }
            else
                parentName = "viewModel";

            foreach (MemberInfo mi in currentType.GetMembers())
            {
                if (mi.MemberType == MemberTypes.Property)
                {
                    PropertyInfo propertyInfo = (PropertyInfo)mi;

                    // no need to include computed properties
                    if (IsComputed(propertyInfo))
                        continue;

                    Type type = propertyInfo.PropertyType;

                    if (type.IsPrimitive || type == typeof(String) || type == typeof(Decimal))
                    {
                        sb.AppendLine("\t\t" + parentName + "." + propertyInfo.Name + "(" + GetJSON(propertyInfo.GetValue(obj, null)) + ");");
                    }
                    else if (typeof(IEnumerable).IsAssignableFrom(type))
                    {
                        object e = propertyInfo.GetValue(obj, null);
                        if (e != null)
                        {
                            foreach (object v in (IEnumerable)e)
                            {
                                sb.AppendLine("\t" + parentName + "." + propertyInfo.Name + ".push(" + GetData(v, level + 1) + ");");
                                sb.AppendLine("");
                            }
                        }
                    }
                    else
                    {
                        object v = propertyInfo.GetValue(obj, null);
                        sb.AppendLine("\t\t" + parentName + "." + propertyInfo.Name + " =" + GetData(v, level + 1) );
                    }
                }
            }

            if (level > 0)
                sb.Append("\t\treturn data" + level1 + "; })()");

            return sb.ToString();
        }

        public MvcHtmlString ApplyBindingsScript(KoBindingOptions options = KoBindingOptions.None)
        {
            Type currentType = this.GetType();

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("<script type=\"text/javascript\">");
            sb.AppendLine("(function () {");

            List<string> includedTypes = new List<string>();

            string typeDefinitions = GetTypesDefinitions(currentType, includedTypes, this);
            sb.AppendLine(typeDefinitions);
            sb.AppendLine("\tvar viewModel = new " + currentType.Name + "();");

            if ((options & KoBindingOptions.IncludeData) == KoBindingOptions.IncludeData)
                sb.AppendLine(GetData(this));

            sb.AppendLine("\tko.applyBindings(viewModel);");
            sb.AppendLine("})();");
            sb.AppendLine("</script>");

            return new MvcHtmlString(sb.ToString());
        }
    }

    [System.AttributeUsage(System.AttributeTargets.Class)]
    public class KoViewModel : System.Attribute
    {
        public KoViewModel(string name)
        {
        }
    }

    [System.AttributeUsage(System.AttributeTargets.Property)]
    public class KoObservable : System.Attribute
    {
        public KoObservable()
        {
        }
    }

    [System.AttributeUsage(System.AttributeTargets.Property)]
    public class KoObservableArray : System.Attribute
    {
        public KoObservableArray()
        {
        }
    }

    [System.AttributeUsage(System.AttributeTargets.Property)]
    public class KoComputed : System.Attribute
    {
        public KoComputed()
        {
        }
    }
}
